package com.panopset.net.ftp;

import static com.panopset.Util.*;
import static com.panopset.UtilIO.*;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.StringTokenizer;
import java.util.Vector;

import com.panopset.Strings;

/**
 * FTP Server.
 *
 * @author Karl Dinwiddie
 *
 */
public final class Server {

    /**
     * Host.
     */
    private final String host;

    /**
     * User.
     */
    private final String usr;

    /**
     * Password.
     */
    private final String pwd;

    /**
     * Remote path.
     */
    private String rmtPath;

    /**
     * Command socket.
     */
    private FTPCommandSocket commandSocket;

    /**
     * Data socket.
     */
    private FTPDataSocket dataSocket;

    /**
     * Server constructor.
     *
     * @param serverHost
     *            Host.
     * @param remotePath
     *            Remote path.
     * @param user
     *            User.
     * @param password
     *            Password.
     * @throws IOException
     *             IO Exception.
     */
    public Server(final String serverHost, final String remotePath,
            final String user, final String password) throws IOException {
        if (!isPopulated(user)) {
            usr = "anonymous";
            pwd = "anonymous";
        } else {
            usr = user;
            pwd = password;
        }
        this.rmtPath = remotePath;
        this.host = serverHost;
    }

    /**
     * Close.
     *
     * @throws IOException
     *             IO Exception.
     */
    public void close() throws IOException {
        getCommandSocket().close();
        commandSocket = null;
        dataSocket = null;
    }

    /**
     * Connect.
     *
     * @throws IOException
     *             IO Exception.
     */
    public void connect() throws IOException {
        dspmsg(sendCommand("USER " + usr));

        dspmsg(sendCommand("PASS " + pwd));
        readLine();
        bin();
        StringTokenizer st = new StringTokenizer(rmtPath, "/");
        while (st.hasMoreTokens()) {
            cwd(st.nextToken());
        }
        readLine();
    }

    /**
     * Change working directory FTP command CWD.
     */
    private static final String CWD = "CWD ";

    /**
     * Change working directory.
     *
     * @param dir
     *            Directory.
     * @return Response.
     * @throws IOException
     *             IO Exception.
     */
    public String cwd(final String dir) throws IOException {
        String response = sendCommand(CWD + dir);
        if (response.startsWith("550 ")) {
            if (response.toLowerCase().indexOf("not a directory") > -1) {
                throw new IOException(response);
            }
            dspmsg(sendCommand("MKD " + dir));
            response = sendCommand(CWD + dir);
            dspmsg(response);
            sendVerify(CWD + dir, "220 ", "200 ", "250 ");
        }
        return response;
    }

    /**
     * Enter binary mode for sending binary files.
     *
     * @throws IOException
     *             IO Exception.
     */
    private void bin() throws IOException {
        sendVerify("TYPE I", "200 ");
    }

    /**
     * Send command.
     * @param command FTP command to send.
     * @return Response from server.
     * @throws IOException IO Exception.
     */
    public String sendCommand(final String command) throws IOException {
        return getCommandSocket().getCommandResponse(command);
    }

    /**
     * Send command and verify.
     * @param command FTP command to send.
     * @param expected Expected response.
     * @return Response from server.
     * @throws IOException IO Exception.
     */
    public String sendVerify(final String command, final String expected)
            throws IOException {
        return getCommandSocket().sendVerify(command, expected);
    }

    /**
     * Send command and verify.
     * @param command FTP command to send.
     * @param expected Possible Responses from server.
     * @return First response from server that matches expected.
     * @throws IOException IO Exception.
     */
    public String sendVerify(final String command, final String... expected)
            throws IOException {
        return getCommandSocket().sendVerify(command, expected);
    }

    /**
     * Get command socket.
     * @return FTPCommandSocket object.
     * @throws IOException IO Exception.
     */
    private FTPCommandSocket getCommandSocket() throws IOException {
        if (commandSocket == null) {
            commandSocket = new FTPCommandSocket(host);
            connect();
        }
        return commandSocket;
    }

    /**
     * Read line.
     * @return Read line.
     * @throws IOException IO Exception.
     */
    private String readLine() throws IOException {
        return getCommandSocket().readLine();
    }

    /**
     * Get file.
     * @param toFile File to write gotten file to.
     * @throws IOException IO Exception.
     */
    public void getFile(final File toFile) throws IOException {
        FTPDataSocket ds = getNewDataSocket();
        String name = toFile.getName();

        String response = sendCommand("RETR " + name);

        if (response.startsWith("550")) {
            dspmsg("Aborting, File not on server.");
            return;
        }

        BufferedInputStream inp = ds.getInputStream();
        response = readLine();
        File parent = toFile.getParentFile();
        if (parent != null && !parent.exists()) {
            mkdirs(parent);
        }
        createNewFile(toFile);
        FileOutputStream fos = new FileOutputStream(toFile);
        BufferedOutputStream output = new BufferedOutputStream(fos);
        byte[] buffer = new byte[ds.getSocket().getReceiveBufferSize()];
        long totalBytesRead = 0;
        int bytesRead = inp.read(buffer);
        while (bytesRead != -1) {
            output.write(buffer, 0, bytesRead);
            totalBytesRead += bytesRead;
            bytesRead = inp.read(buffer);
        }
        output.flush();
        output.close();
        ds.close();
        dspmsg(toFile.getName() + " bytes:" + totalBytesRead);
        response = readLine();
        if (!response.startsWith("226 ")) {
            throw new IOException("Unexpected response: " + response);
        }
    }

    /**
     * FTP list command.
     *
     * @return List of remote files.
     * @throws IOException
     *             IO Exception.
     */
    public synchronized Vector<String> list() throws IOException {
        FTPDataSocket ds = getNewDataSocket();
        Vector<String> rtn = new Vector<String>();
        String response = null;

        BufferedInputStream inp = ds.getInputStream();

        sendCommand("LIST");

        response = readLine();
        System.out.println(response);
        byte[] buffer = new byte[ds.getSocket().getReceiveBufferSize()];
        long totalBytesRead = 0;
        int bytesRead = inp.read(buffer);
        StringBuffer sb = new StringBuffer();
        int lastLineFeed = -1;
        while (bytesRead != -1) {
            int i = 0;
            while (i < bytesRead) {
                if (buffer[i] == Strings.CARRIAGE_RETURN.charAt(0)
                    || buffer[i] == Strings.LINE_FEED.charAt(0)) {
                    if (i != lastLineFeed) {
                        rtn.add(sb.toString());
                        sb = new StringBuffer();
                        i = lastLineFeed;
                    }
                } else {
                    sb.append((char) buffer[i]);
                }
                i++;
            }
            totalBytesRead += bytesRead;
            bytesRead = inp.read(buffer);
        }
        if (sb.length() > -1) {
            rtn.add(sb.toString());
        }

        readLine();
        ds.close();
        return rtn;
    }

    /**
     *
     * @param inputStream Input stream.
     * @param filename File name.
     * @return true if successful.
     * @throws IOException IO Exception.
     */
    public boolean stor(final InputStream inputStream, final String filename)
            throws IOException {
        String response = null;

        FTPDataSocket ds = getNewDataSocket();

        sendCommand("STOR " + filename);

        BufferedOutputStream outputStream = ds.getOutputStream();

        readLine();

        byte[] buffer = new byte[ds.getSocket().getSendBufferSize()];
        int bytesRead = 0;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        outputStream.flush();
        outputStream.close();
        inputStream.close();

        response = readLine();
        return response.startsWith("226 ");
    }

    /**
     * Data port info.
     */
    static final class DataPortInfo {
        /**
         * Port.
         */
        private Integer port;
        /**
         * IP address.
         */
        private String ip;
        /**
         * Prevent outside instantiation.
         */
        private DataPortInfo() { }
    }

    /**
     * Data port info.
     */
    private DataPortInfo dataPortInfo;

    /**
     * Port offset.
     */
    private static final int PORT_OFFSET = 256;

    /**
     * Get data port.
     *
     * @return Data port.
     * @throws IOException
     *             IO Exception.
     */
    private DataPortInfo getDataPort() throws IOException {
        if (dataPortInfo == null) {
            dataPortInfo = new DataPortInfo();
            String response = sendCommand("PASV");
            if (response == null) {
                return null;
            }
            int opening = response.indexOf('(');
            int closing = response.indexOf(')', opening + 1);
            if (closing > 0) {
                String dataLink = response.substring(opening + 1, closing);
                StringTokenizer tokenizer = new StringTokenizer(dataLink, ",");
                try {
                    dataPortInfo.ip = tokenizer.nextToken() + "."
                            + tokenizer.nextToken() + "."
                            + tokenizer.nextToken() + "."
                            + tokenizer.nextToken();
                    dataPortInfo.port = Integer.parseInt(tokenizer.nextToken())
                            * PORT_OFFSET
                            + Integer.parseInt(tokenizer.nextToken());
                } catch (Exception e) {
                    log(e);
                }
            } else {
                throw new IOException("Unexpected response: " + response);
            }
        }
        return dataPortInfo;
    }

    /**
     * Send the PASV command to port 21 to get the data socket port.
     *
     * @return DataSocket
     * @throws IOException
     *             IO Exception.
     */
    private FTPDataSocket getNewDataSocket() throws IOException {
        if (dataSocket != null) {
            dataSocket.close();
        }
        dataPortInfo = null;
        dataSocket = new FTPDataSocket(getDataPort().ip, getDataPort().port);
        return dataSocket;
    }

    @Override
    public String toString() {
        return "commandSocket: " + commandSocket + "\n dataSocket: "
                + dataSocket;
    }

}
